iT邦幫忙

2024 iThome 鐵人賽

DAY 9
0
Mobile Development

從零開始以Flutter打造跨平台聊天APP系列 第 9

Day-9 在 Flutter 中使用 InheritedWidget 進行狀態管理

  • 分享至 

  • xImage
  •  

Generated from Stable Diffusion 3 Medium

昨天提到的 StatefulWidget 會使用 createState 建立一個狀態,而設計師們可以再設計一個 build 函式建立 Widget。當使用者觸發 setState 時,會使得 該 Widget 被重新 build()。試想,假設今天在 Widget A 觸發了一個按鈕,我們希望 Widget B 可以被更新。然而 Widget B 可能被包在更多的 Widget 之中,這時我們只能,一個傳一個,把資料傳遞給 Widget B。在 Flutter 中,我們可以用 InheritedWidget 來處理這個煩人的問題!

https://ithelp.ithome.com.tw/upload/images/20240910/20129540InqjD48Eki.png

圖片來源:https://www.youtube.com/watch?v=Zbm3hjPjQMk&t=50s

本章節範例程式碼:https://github.com/ksw2000/ironman-2024/tree/master/flutter-practice/inherited_widget

InheritedWidget

InheritedWidget 有以下幾個使用場景:

  1. 全局狀態管理
    • 在整個應用中共享資料或狀態,而不必手動將這些數據傳遞給每一個 widget。
    • 如用戶登入資訊、主題設定、應用語言設定等需要在不同頁面或組件之間共享的資料。
  2. 跨越多層 widget 的數據共享
    • 如果某個深層次的 widget 需要訪問來自祖先 widget 的資料或狀態,直接使用參數傳遞會導致很多重複的代碼。InheritedWidget 可以解決這個問題,讓子 widget 可以直接訪問上層的數據。
  3. 高效的局部重繪
    • InheritedWidget 只會通知那些真正依賴於其數據變化的子 widget 重繪,而不會影響其他不依賴的 widget,這有助於提高應用性能。

以下是 InheritedWidget 的大致用方式,我們可以把 widgetA 和 widgetB 都用 InheritedWidget 包裝,當然 widgetB 可能被包進更深處,比如 widgetC。

class InheritedCounter extends InheritedWidget {
  // ...
}

class ... {
  return InheritedCounter(
    child: ...
      widgetC
      widgetA
  );
}

class widgetC ... {
  build () {
    return
      widgetB
  }
}

InheritedWidget 負責管理「資料」,當 widgetA 利用 setState 更動資料時,InheritedWidget 會將改動的部分通知 widgetB。以下是 InheritedWidget 的寫法。

class InheritedCounter extends InheritedWidget {
  const InheritedCounter({super.key, required this.counter, required super.child});
  final int counter;

  static InheritedCounter? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedCounter>();
  }

  @override
  bool updateShouldNotify(InheritedCounter oldWidget) {
    return oldWidget.counter != counter;
  }
}

首先 updateShouldNotify 代表何時需要通知重建,當舊 widget 中的 count 值不等於當前 widget 的 count 值時,就要進行畫面重新。而另一個靜態函式 of 這個用法是讓需被更新的 widget (比如 widgetB),直接取得資料,而不需要透過中間的 widget (比如 widgetC)。

我們可以更改昨天的範例程式,移除 FloatingActionButton,並另外建構一個 MyTextMyButton

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: InheritedCounter(
            count: _counter,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                const MyText(),
                const SizedBox(
                  height: 10,
                ),
                MyButton(
                  // TODO
                ),
              ],
            )),
      ),
    );
  }
}

首先,我們先處理 MyText 的部分,MyText 可以利用 InheritedCounter 中的 of 方法取得 counter

class MyText extends StatefulWidget {
  const MyText({super.key});

  @override
  State<MyText> createState() => _MyTextState();
}

class _MyTextState extends State<MyText> {
  @override
  Widget build(BuildContext context) {
    return Text(
      // 呼叫 InheritedCounter 中 of 靜態方法,取得其「實例」
      // 並將 counter 欄位讀出轉出字串
      InheritedCounter.of(context)!.counter.toString(),
      style: Theme.of(context).textTheme.headlineMedium,
    );
  }
}

接著我們處理 MyButon 的部分。由於在 MyButton 的 scope 呼叫 setState() 並無意義,且 MyButton 根本不是 StatefulWidget,因此我們可以將 onPressed 由更上層的 Widget 實作,再往下傳遞,這樣子當按下按鈕時,觸發的 onPressed 就會觸發 _MyHomePageState 中的 setState()

class MyButton extends StatelessWidget {
  const MyButton({super.key, this.onPressed});
  final void Function()? onPressed;

  @override
  Widget build(BuildContext context) {
    return TextButton(onPressed: onPressed, child: const Text('Inceasement'));
  }
}

剛剛的範例在 _MyHomePageState 中省略了 MyButtononPressed 參數,現在我們將其補齊。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // ...
                MyButton(
                  onPressed: () {
                    setState(() {
                      _counter++;
                    });
                  },
                ),
      // ...
    );
  }
}

Flutter 3.7 maybeOf() & of()

我們可以發現在 InheritedCounter of 回傳的是一個可為 null 的物件,導致我們在調用時必需加入 ! 。在新版 flutter 中建議將這種寫法取名叫 maybeOf()of() 則是直接回傳一個不可為 null 的物件。其實就是直接在原本的函式中先做斷言。

參考官方文件:Migrate of to non-nullable return values, and add maybeOf | Flutter

因此我們可以再次更改程式碼:

class InheritedCounter extends InheritedWidget {
  const InheritedCounter(
      {super.key, required this.counter, required super.child});
  final int counter;

  static InheritedCounter? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<InheritedCounter>();
  }

  static InheritedCounter of(BuildContext context) {
    final result =
        context.dependOnInheritedWidgetOfExactType<InheritedCounter>();
    assert(result != null, 'No InheritedCounter found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(InheritedCounter oldWidget) {
    return oldWidget.counter != counter;
  }
}

Flutter 內的 Theme.of()

有時候,我們會將 InheritedWidget 的實作設為 private,但此時又希望 of 方法可以被調用,此時我們可以將 of 的實作改移到更上層的 widget 中。比如 Theme.of() 的實作是在 Theme 中,而 Theme 是一個 StatelessWidget

class Theme extends StatelessWidget {
  // of 方法在 Theme 實作
  static ThemeData of(BuildContext context) {
    // _InheritedTheme 是一個繼承 InheritedWidget 的 class
    // 也就是說 of 這個方法是由另一個 class 來實作而不是直接在 _InheritedTheme 裡實作
    // 這樣子可以不讓其他套件引用 _InheritedTheme 但也保留了 of 的功能取得資料
    final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
    // ...
    return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
  }
    
  @override
  Widget build(BuildContext context) {
    return _InheritedTheme(
      // ...
    );
  }
}

_InheritedTheme 中並沒有實作 of 這個靜態方法

class _InheritedTheme extends InheritedTheme {
  const _InheritedTheme({
    required this.theme,
    required super.child,
  });

  final Theme theme;

  @override
  Widget wrap(BuildContext context, Widget child) {
    return Theme(data: theme.data, child: child);
  }

  @override
  bool updateShouldNotify(_InheritedTheme old) => theme.data != old.theme.data;
}

後記:

AI 沒辦法理解我要他畫 widget tree,他只會畫 tree

當我們的 widget 樹越來越複雜時,資料的傳遞和畫面的更新也會變得很複雜,雖然物件導向的設計可以讓整個程式更有條理,但在 Flutter 中狀態的管理還是很不容易,明天會提到除了用 InheritedWidget 管理狀態,我們也可以使用 Riverpod 進行管理。

喜歡我的朋友別忘了去追蹤我的 Github: @ksw2000 追蹤系列文及按讚這篇文章!


上一篇
Day-8 建構第一個 Flutter APP
下一篇
Day-10 Flutter路由管理
系列文
從零開始以Flutter打造跨平台聊天APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言